

from datetime import date, datetime, timedelta
from trash import send2trash
from types import NoneType
import autotrash
import gobject
import gtk
import kronos
import logging
import logging.config
import logging.handlers
import os.path
import re
import shlex
import shutil
import subprocess
import sys
import time
import yaml
import zipfile



__author__ = "zainul"
__date__ = "$23/06/2010 10:04:04 PM$"

# public constants
FILE_NAME = "Name"
FILE_EXT = "Extension"
FILE_SIZE = "Size"
LAST_MOD_DT = "Date Last Modified"
LAST_OPENED_DT = "Date Last Opened"
CREATE_DT = "Date Created"

IS = "Is"
IS_NOT = "Is Not"
CONTAINS = "Contains"
DOES_NOT_CONTAIN = "Does Not Contain"
EQUAL = "Equal"
NOT_EQUAL = "Not Equal"
GREATER_THAN = "Greater Than"
LESS_THAN = "Less Than"
IS_IN_THE_LAST = "Is In the last"
IS_NOT_IN_THE_LAST = "Is Not In The Last"
MATCHES_ONE_OF = 'Matches One Of'
DOES_NOT_MATCH_ONE_OF = 'Does Not Match One Of'  

ALL = "ALL"
ANY = "ANY"

VALIDATION_SAME_RULE = "The rule already exist"
VALIDATION_NO_DESC = "Specify a description"
VALIDATION_INVALID_RULE = "Some rule conditions are invalid"
VALIDATION_INVALID_VERB = "Select an operation";
VALIDATION_INVALID_NEW_FILE_NAME = "Please specify a new file name";

CONFIG_FILE_DIR = os.getenv("HOME") + '/.Miranda/'
CONFIG_FILE_NAME = 'config.yaml' 
LOG_FILENAME = CONFIG_FILE_DIR + 'miranda.log'

MINUTES = "Minutes"
HOURS = "Hours"
DAYS = "Days"
WEEKS = "Weeks"
       
__scheduler_instance = None    
observer = None
logger = None
main_screen = None

def do_file_operation(rule):
                
    
    """
    Perform the specified file operation in the provided rule_subjects list on every files
    inside the specified source folder in the rule_setting instance.
    """
    
    rule_setting = rule.rule_setting;
    rule_verb = rule.rule_verb
    rule_subjects = rule.rule_subjects
    rule_condition = rule.rule_condition
    
    source_folder = rule.rule_setting.source_folder
    files = os.listdir( source_folder );
    
    need_confirmations = [];    
    
    global main_screen
    global logger
            
    for eachFile in files:
                       
        infile = os.path.join(source_folder,eachFile)
        
        if os.path.isdir(infile) and (not rule_setting.recursive):
            continue;
        elif os.path.isdir(infile) and (rule_setting.recursive): 
            rule_setting.source_folder = infile;
            do_file_operation(rule);
        else:
            if (not rule_setting.enabled):
                continue;
            
            if(__does_file_satisfy_rule__(rule_subjects,rule_condition,infile)):                        
                logger.debug('Processing :' + infile + ' for ' + rule_setting.description)    
                if(not rule_setting.confirm_action) :
                    action_file(rule_verb,infile);                    
                else:                    
                    need_confirmations.append(infile);
            else:
                logger.debug(infile + ' does not satisfy rule: ' + rule_setting.description)
                     
                
    if (len(need_confirmations) > 0):
                           
        msg = 'This Rule matches the following file(s): \n' + ','.join([(os.path.split(infile)[1]) for infile in need_confirmations]);
        
        def on_dialog_key_press():            
            
            for eachFile in need_confirmations:
                action_file(rule_verb,eachFile);                

        open_confirm_action_dialog(on_dialog_key_press, msg,main_screen.window);
        
                           
def open_confirm_action_dialog(callback, msg, parent_window=None):
                     
    gtk.gdk.threads_init()                        
        
    gtk.gdk.threads_enter()
    
    def ok_cancel_action(dialog, event):        
               
        if (event == gtk.RESPONSE_CANCEL):
            return;
                        
        if (event == gtk.RESPONSE_OK):            
            callback();
                               
    md = gtk.MessageDialog(parent_window, gtk.DIALOG_MODAL, 
                           gtk.MESSAGE_WARNING, gtk.BUTTONS_OK_CANCEL, msg);    
    md.connect('response', ok_cancel_action)    
    md.run()
    md.destroy()
    
    gtk.gdk.threads_leave()
 
    return;
        
def __does_file_satisfy_rule__(rule_subjects,rule_condition,file):
    
    """
    This method returns True if the specified file satisfies the provided rule subjects and rule_condition
    """
    
    for eachRule in rule_subjects:
            
            fileCmpToCompare = get_file_component(file,eachRule.file_component_type);        
            fileMatchesRule = match(fileCmpToCompare, eachRule.criteria, eachRule.value,eachRule.uom); 
            
            if(fileMatchesRule and rule_condition == ANY):
                return True;
            if(not fileMatchesRule and rule_condition == ALL):
                return False;
            
    if(rule_condition == ANY):
        return False
    
    return True;
            
def get_file_component(path, file_attr):
    
    """
    Return a file component name based on the specified file attribute.
    
    File Component can either be file name, file extension, file size, last modified date, last opened date,
    create date.
    """
    
    if file_attr == FILE_NAME:
        (fileBaseName, fileExtension)=os.path.splitext(os.path.basename(path))
        return fileBaseName;
    elif file_attr == FILE_EXT:
        (fileBaseName, fileExtension)=os.path.splitext(os.path.basename(path))        
        return fileExtension.lstrip('.');
    elif file_attr == FILE_SIZE:
        return os.path.getsize(path);
    elif file_attr == LAST_MOD_DT:
        stat = os.stat(path);
        return stat.st_mtime;
    elif file_attr == LAST_OPENED_DT:
        stat = os.stat(path);
        return stat.st_atime;
    elif file_attr == CREATE_DT:
        stat = os.stat(path);
        return stat.st_ctime;

def match (file_cmp_to_compare, criteria,value,uom=NoneType):

    """
    Return True when the specified file component (file_cmp_to_compare) matches the provided value.
    """
    
    """
    convert uom to bytes for comparison
    """
    if(criteria == EQUAL or criteria == NOT_EQUAL or criteria == GREATER_THAN or criteria == LESS_THAN):
        if(uom == RuleSubject.KB):
            file_cmp_to_compare = long(file_cmp_to_compare)/1024
        elif(uom == RuleSubject.MB):
            file_cmp_to_compare = long(file_cmp_to_compare)/1048576
    
    time_period = 0;
    time_period_ts = 0;
        
    if(criteria == IS_IN_THE_LAST or criteria == IS_NOT_IN_THE_LAST):
        if(uom == RuleSubject.MINUTE):
            time_period = timedelta (minutes= - int(value))
        elif(uom == RuleSubject.HOUR):
            time_period = timedelta (hours= - int(value))
        elif(uom == RuleSubject.WEEK):
            time_period = timedelta (weeks= - int(value))
        elif(uom == RuleSubject.MONTH):
            time_period = timedelta (days= - (int(value) * 30))
        elif(uom == RuleSubject.MONTH):
            time_period = timedelta (days= - (int(value) * 365))
                
        time_period_ts = time.mktime((datetime.now() + time_period ).timetuple())
        
        
    if(criteria == IS):
        return file_cmp_to_compare.lower() == value.lower();
    elif(criteria == IS_NOT):
        return file_cmp_to_compare.lower() != value.lower();
    elif(criteria == CONTAINS):
        match = (file_cmp_to_compare.lower()).find(value.lower())
        return match > -1;
    elif(criteria == DOES_NOT_CONTAIN):
        return ((file_cmp_to_compare.lower()).find(value.lower()) == -1);
    elif(criteria == EQUAL):       
        return long(file_cmp_to_compare) == long(float(value));
    elif(criteria == NOT_EQUAL):       
        return long(file_cmp_to_compare) != long(float(value));
    elif(criteria == GREATER_THAN):
        return long(file_cmp_to_compare) > long(float(value));
    elif(criteria == LESS_THAN):
        return long(file_cmp_to_compare) < long(float(value));
    elif(criteria == IS_IN_THE_LAST):
        return file_cmp_to_compare >= time_period_ts        
    elif(criteria == IS_NOT_IN_THE_LAST):
        return file_cmp_to_compare < time_period_ts 
    elif(criteria == MATCHES_ONE_OF):
                
        my_splitter = shlex.shlex(value, posix=True)
        my_splitter.whitespace = ','
        my_splitter.whitespace_split = True
        
        for file_name in my_splitter:
            if(file_cmp_to_compare.lower() == file_name.lower()):
                return True
        return False;
    
    elif(criteria == DOES_NOT_MATCH_ONE_OF):
        
        my_splitter = shlex.shlex(value, posix=True)
        my_splitter.whitespace = ','
        my_splitter.whitespace_split = True
        
        for file_name in my_splitter:            
            if(file_cmp_to_compare.lower() == file_name.lower()):
                return False
        return True;
        
def check_overwrite(operation,file):
    
    """
    Return True when use specified 'Overwrite' in the rule verb section
    Return False when a file with the same name exist in the destination folder and user specified no overwrite
    """
    
    file_name = os.path.split(file)[1]
        
    if(operation.type == operation.MOVE and file_exist(operation.dest_folder, file_name) and not operation.overwrite):
        return False;
   
    return True;
        
def action_file(rule_verb, file):
    
    if(rule_verb.type == rule_verb.COPY):
        logger.debug('Copying file:'  + file + ' to :' + rule_verb.dest_folder)
        shutil.copy2(file, rule_verb.dest_folder);
        return 1
    elif(rule_verb.type == rule_verb.MOVE and check_overwrite(rule_verb,file) and (not rule_verb.compression)):
        logger.debug('Move file:'  + file + ' to :' + rule_verb.dest_folder)
        shutil.copy2(file, rule_verb.dest_folder);
        os.remove(file);
        return 1
    elif(rule_verb.type == rule_verb.MOVE and check_overwrite(rule_verb,file) and (rule_verb.compression)):
        
        logger.debug('Zipping and Moving file:'  + file + ' to :' + rule_verb.dest_folder)
        
        tail = os.path.split(file)[1]
        file_name = os.path.splitext(tail)[0]
        
        zip_file_path = rule_verb.dest_folder + '/' + file_name + '.zip'
        
        z = zipfile.ZipFile(zip_file_path, "w")
        z.write(file)
        z.close();
        
        os.remove(file);       
        return 1
    elif(rule_verb.type == rule_verb.DELETE):
        logger.debug('Deleting file:'  + file)
        os.remove(file);
        return 1
    elif(rule_verb.type == rule_verb.RENAME):        
        (dirName, fileName) = os.path.split(file)
        fileExtension = os.path.splitext(fileName)[1]
        
        logger.debug('Renaming file:'  + file + ' to :' + dirName + '/' + rule_verb.new_file_name + fileExtension)
        os.rename(file, dirName + '/' + rule_verb.new_file_name + fileExtension);
        
        return 1
    elif(rule_verb.type == rule_verb.RECYCLE):
        logger.debug('Sending file:'  + file + ' to trash')
        send2trash(file);
        return 1;
    elif (rule_verb.type == rule_verb.OPEN):
        logger.debug('Opening file:'  + file)
        subprocess.call(('gnome-open', file))
    return -1;

def file_exist(directory,file_name):
    file_path = os.path.join(directory,file_name)
    return os.path.isfile(file_path)

def read_config(file=CONFIG_FILE_DIR + CONFIG_FILE_NAME):
    
    config_file = open(file,'r');
    config = yaml.load(config_file)
    config_file.close();
    
    if isinstance(config, NoneType):
        return Config()
    
    return config

def validate_rule(rule,file=CONFIG_FILE_DIR + CONFIG_FILE_NAME):
    
    if rule.rule_setting.description == '':
        return VALIDATION_NO_DESC
 
    # check to make sure that the rule subjects are valid
    if rule.rule_subjects:
        for subject in rule.rule_subjects:
            if not validate_rule_subject(subject):
                return VALIDATION_INVALID_RULE
    
    # check that the user at least select one rule verb
    if isinstance(rule.rule_verb.type, NoneType):
        return VALIDATION_INVALID_VERB
    elif rule.rule_verb.type == RuleVerb.RENAME and rule.rule_verb.new_file_name == '':
        return VALIDATION_INVALID_NEW_FILE_NAME
    
    if not rule.isNew :
        return None
    
    # check to make sure that the rule has some conditions
    
    rule_dict = read_config(file).rule_dict;
    
    if not rule_dict.has_key(rule.rule_setting.source_folder):
        return None
    
    rules = rule_dict.get(rule.rule_setting.source_folder)
    
    try:
        rules.index(rule)
        return VALIDATION_SAME_RULE
    except ValueError:
        return None

def validate_rule_subject (subject):
    # check to see if the necessary values are provided. just sanity checking! :)
    if not (subject.file_component_type and subject.criteria and subject.value):
        return False
    
    # check to see if the value we are checking for requires a UOM (??)
    if (subject.file_component_type == LAST_MOD_DT or 
        subject.file_component_type == LAST_OPENED_DT or
        subject.file_component_type == CREATE_DT or
        subject.file_component_type == FILE_SIZE):
        if not (subject.uom):
            return False
        # see if the condition is file size
        if subject.file_component_type == FILE_SIZE:
            try:
                float(subject.value)
            except ValueError:
                return False
        else: # otherwise, it's a date related condition
            if not subject.uom.isdigit(): # make sure it's an integer without a decimal component
                return False
            
    # signal a validated rule subject
    return True

observer = None;


def notify_observers():
    
    if not isinstance(observer, NoneType):
        observer.notify();
            
def dump_rules(config, file=CONFIG_FILE_DIR + CONFIG_FILE_NAME):
    
    (dirName, fileName) = os.path.split(file)
    
    if not os.path.isdir(dirName):
        os.mkdir(dirName)
        
    if not os.path.isfile(fileName):
        config_file = open(fileName,'w');
        config_file.write("");
          
    config_file = open(file,'w');    
    
    keys = config.rule_dict.keys();
    
    #set the isNew indicator for each rule to false
    for each_key in keys:
        rules = config.rule_dict.get(each_key)
        for each_rule in rules:
            each_rule.isNew = False;
                    
    yaml.dump(config,config_file)              
    config_file.close();    

def open_msg_dialog(parent_window,msg):
        
    md = gtk.MessageDialog(parent_window, 
    gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_INFO, 
    gtk.BUTTONS_CLOSE, msg)
    md.run()
    md.destroy()
    return              

def init_logging():
    
    logging.config.fileConfig("./logging.conf")
    global logger
    
    logger = logging.getLogger("logger")

    handler = logging.handlers.RotatingFileHandler(LOG_FILENAME, maxBytes=1000, backupCount=10)
    
    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    handler.setFormatter(formatter)
    
    logger.addHandler(handler)


def create_config_file():
    if not os.path.isdir(CONFIG_FILE_DIR):
        os.mkdir(CONFIG_FILE_DIR)
        
    if not os.path.isfile(CONFIG_FILE_DIR + CONFIG_FILE_NAME):
        config_file = open(CONFIG_FILE_DIR + CONFIG_FILE_NAME,'w');
        config_file.write("");
        
    if not os.path.isfile(LOG_FILENAME):
        log_file = open(LOG_FILENAME,'w');
        log_file.write("");
        
def empty_rubbish_bin ():
    autotrash.main('--delete 100')
            
class Scheduler:
    
    def __init__(self):
        self.config = None;
                                    
    def __execute_rules__(self):
                
        rule_dict = self.config.rule_dict
        key_list = rule_dict.keys()
       
        for each_key in key_list:
            rules = rule_dict.get(each_key)
           
            for each_rule in rules:
                do_file_operation(each_rule)
                   
    def __execute_bin_mgr__(self):

        bin_config = self.config.bin_manager
        interval_in_minutes = int(bin_config.interval);
               
        if bin_config.uom == HOURS:
            interval_in_minutes = bin_config.interval * 60
        elif bin_config.uom == DAYS:
            interval_in_minutes = bin_config.interval * 24 * 60
        elif bin_config.uom == WEEKS:
            interval_in_minutes = bin_config.interval * 7 * 24 * 60
                        
        if bin_config.is_empty_recycle_bin_enabled and  datetime.now() + timedelta (minutes=- interval_in_minutes) >= bin_config.first_enabled_time:
            sys.argv = ["Miranda","--delete","1"]
            autotrash.main(sys.argv)                         
            bin_config.first_enabled_time = datetime.now()
            self.config.bin_manager = bin_config;        
        
    def schedule(self):        
        
        self.schedule = kronos.Scheduler();            
        self.rule_task = self.schedule.add_interval_task(self.__execute_rules__, 'Schedule Rule', 5, self.config.sleeptime, kronos.method.sequential, [], {});            
        self.bin_mgr_task = self.schedule.add_interval_task(self.__execute_bin_mgr__, 'Schedule Bin Mgr', 5, self.config.sleeptime, kronos.method.sequential, [], {});
        
        
        self.schedule.start();        
    
    def resume_file_processing_schedule(self):
        self.schedule.start();
        
    def cancel_file_processing_schedule(self):
        self.schedule.cancel(self.rule_task)
        self.schedule.cancel(self.bin_mgr_task)
    
    def stop (self):
        self.schedule.stop()
        
class RuleSetting:
    description="";
    source_folder="";
    enabled = False;
    confirm_action = False;
    recursive = False;
                 
class RuleVerb:
    MOVE = "Move File";
    RENAME = "Rename File";
    COPY = "Copy File"
    DELETE = "Delete File"
    RECYCLE = "Send to Recycle Bin";
    OPEN = "Open file"
    
    type = "";
    dest_folder = "";
    new_file_name = "";
    overwrite = False;
    compression = False;
    
class RuleSubject:
        
    B = "Byte"
    KB = "KiloByte"
    MB = "MegaByte"

    MINUTE = "MINUTE"
    HOUR = "HOUR"
    DAY = "DAY"
    MONTH = "MONTH"
    WEEK = "WEEK"
    YEAR = "YEAR"

    file_component_type = "";
    uom = "";
    value = "";
    criteria = "";
    file_name_list="";    
     
    def get_value(self):
        if(self.uom == self.MINUTE):
            return self.value * 60;
        elif (self.uom == self.HOUR):
            return self.value * 60 * 60;
        elif (self.uom == self.DAY):
            return self.value * 24 * 3600;
        elif (self.uom == self.WEEK):
            return self.value * 7 * 24 * 3600;
        elif (self.uom == self.MONTH):
            return self.value * 30 * 24 * 3600;
        elif (self.uom == self.YEAR):
            return self.value * 365 * 24 * 3600;
        elif (self.uom == self.B):
            return self.value;
        elif (self.uom == self.KB):
            return self.value * 1024;
        elif (self.uom == self.MB):
            return self.value * 1024 * 1024;
        else:
            x_re = re.compile(r'[,]')
            return x_re.split(self.file_name_list);
           
class BinManager:
    
    def __init__(self):
        self.is_empty_recycle_bin_enabled = False
        self.is_bin_mgmt_enabled = False
        self.interval = 0;
        self.uom = None
        self.first_enabled_time = None
        
class Rule:
    
    def __init__(self):
        self.rule_subjects = [];
        self.rule_setting = RuleSetting()
        self.rule_verb = RuleVerb()    
        self.rule_condition = '';         
        #indicate that this rule has not been saved into the flat file
        self.isNew = True;
          
         
    def __eq__(self, other):
        if (isinstance(other, Rule)):
            return ((self.rule_setting.source_folder == other.rule_setting.source_folder)
                    and (self.rule_setting.description == other.rule_setting.description))
        else:
            return NotImplemented
            
    def __ne__(self, other):
        equal_result = self.__eq__(other)
        if (equal_result is not NotImplemented):
            return not equal_result
        return NotImplemented
         
class Config:
    
    def __init__(self):
        self.rule_dict = {};
        self.bin_manager = BinManager();
        self.sleeptime = 5;

def __run_scheduler__():
    scheduler = Scheduler();
    scheduler.schedule()

            


